﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Citrix.XaAdminMobile.Model;
using System.ComponentModel;
using Citrix.Cmp;
using Citrix.XenApp.Commands;
using Citrix.XaAdminMobile.Util;

namespace Citrix.XaAdminMobile.ViewModel
{
    /// <summary>
    /// View-model class for displaying a single XenApp session. This is used to display the
    /// list of sessions in SessionsView as well as the session details in SessionDetailsView.
    /// </summary>
    internal class SessionViewModel : INotifyPropertyChanged, IPage
    {
        /// <summary>
        /// The associated model object.
        /// </summary>
        private readonly ISessionModel sessionModel;

        /// <summary>
        /// A reference back to the root v-m object.
        /// </summary>
        private readonly ViewModelManager vmRoot;

        /// <summary>
        /// A set of all the session details. Item1 is 
        /// </summary>
        private List<Tuple<string, string>> sessionDetails;

        /// <summary>
        /// Used to sync access to internal state.
        /// </summary>
        private object stateLock = new object();

        /// <summary>
        /// Whether or not this session has been selected.
        /// </summary>
        private bool isSelectedForEdit = false;

        /// <summary>
        /// Used to display a loading status on the session details view.
        /// </summary>
        private LoadingViewModel loadingStatus;

        /// <summary>
        /// Initialisation ctor.
        /// </summary>
        /// <param name="vmMgr">The base v-m object.</param>
        /// <param name="sessionSource">The associated session model object.</param>
        /// <exception cref="ArgumentNullException">vmMgr, sessionSource</exception>
        public SessionViewModel(ViewModelManager vmMgr, ISessionModel sessionSource)
        {
            if (null == vmMgr)
            {
                throw new ArgumentNullException("vmMgr");
            }
            if (null == sessionSource)
            {
                throw new ArgumentNullException("sessionSource");
            }

            this.vmRoot = vmMgr;
            this.sessionModel = sessionSource;
            this.sessionModel.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(SessionModel_PropertyChanged);
            this.IsSelectedForEdit = false;
            this.DisplaySessionDetailsCommand = new DelegateCommand(() =>
            {
                this.vmRoot.NavigateToPage(this);
            });
            this.RefreshCommand = new DelegateCommand(() => this.sessionModel.Refresh(true));
            this.ResetCommand = new DelegateCommand(() =>
            {
                List<ISessionModel> args = new List<ISessionModel>();
                args.Add(this.sessionModel);
                this.sessionModel.Parent.Reset(args);

                this.vmRoot.DefaultBackCommand.Execute();
            });
            this.RemoteAssistCommand = new DelegateCommand(() => RemoteAssistSession());

            this.loadingStatus = new LoadingViewModel(() => { return this.sessionModel.IsLoading; });
            
            CreateSessionDetails();
        }

        /// <summary>
        /// The name of the user account that owns this session.
        /// </summary>
        public string AccountName
        {
            get { return this.sessionModel.AccountName; }
        }

        /// <summary>
        /// The back command used when this session is displayed on the SessionDetailsView.
        /// </summary>
        public DelegateCommand BackCommand
        {
            get { return this.vmRoot.DefaultBackCommand; }
        }

        /// <summary>
        /// The name of the published application associated with this session
        /// </summary>
        public string BrowserName
        {
            get
            {
                return this.sessionModel.BrowserName;
            }
        }

        /// <summary>
        /// Compares this session with another session instance to produce the display order.
        /// </summary>
        /// <param name="source">The source to compare against.</param>
        /// <returns>-1 if we are less than source, 0 if equal, +1 if we are greater than
        /// source.</returns>
        public int Compare(SessionViewModel source)
        {
            if (null == source)
            {
                return -1;
            }

            int result = -1;

            // Compare on user name first.
            result = string.CompareOrdinal(this.AccountName, source.AccountName);
            if (0 == result)
            {
                // Then session ID.
                result = Nullable.Compare(this.SessionId, source.SessionId);
                if (0 == result)
                {
                    // Then server name.
                    result = string.CompareOrdinal(this.ServerName, source.ServerName);
                }
            }

            return result;
        }

        /// <summary>
        /// Used on the SessionsView to display this session in the details view.
        /// </summary>
        public DelegateCommand DisplaySessionDetailsCommand
        {
            get;
            private set;
        }

        /// <summary>
        /// Returns the state of the toggle selection mode. By default it is disabled and
        /// selecting an item will show the details for the session. When enabled selecting a
        /// session will mark it as "selected" in order to perform a reset on it.
        /// </summary>
        public bool EnableToggleSelect
        {
            get
            {
                return this.vmRoot.SessionsViewModel.IsToggleSelectActive;
            }
        }

        /// <summary>
        /// Whether or not this item is selected on the UI. Used to track which sessions have
        /// been selected by the user when executing commands.
        /// </summary>
        public bool IsSelectedForEdit
        {
            get
            {
                return this.isSelectedForEdit;
            }
            set
            {
                if (this.isSelectedForEdit != value)
                {
                    this.isSelectedForEdit = value;
                    OnPropertyChanged("IsSelectedForEdit");
                }
            }
        }

        /// <summary>
        /// Bound to the associated UI element IsEnabled property to control the enabled state.
        /// We disable the UI element whenever we are performing some action on this session.
        /// </summary>
        public bool IsEnabled
        {
            get
            {
                return this.sessionModel.SessionActionStatus == SessionActionStatus.None;
            }
        }

        /// <summary>
        /// Used to display a loading status on the session details view.
        /// </summary>
        public LoadingViewModel LoadingStatus
        {
            get { return this.loadingStatus; }
        }

        /// <summary>
        /// Checks whether this session matches the supplied search text for the supplied search
        /// target.
        /// </summary>
        /// <param name="searchText">The search text to match against.</param>
        /// <param name="target">Defines the fields to search against.</param>
        /// <returns>True if this session is match, false if it isn't.</returns>
        public bool Match(string searchText, SessionSearchOptions target)
        {
            if (string.IsNullOrEmpty(searchText))
            {
                return false;
            }

            bool matched = false;
            if ((target & SessionSearchOptions.App) == SessionSearchOptions.App)
            {
                string appName = this.BrowserName;
                if (null == appName)
                {
                    appName = string.Empty;
                }
                matched = appName.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0;
            }
            if (!matched && ((target & SessionSearchOptions.User) == SessionSearchOptions.User))
            {
                string userName = this.AccountName;
                if (null == userName)
                {
                    userName = string.Empty;
                }
                matched = userName.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0;
            }
            if (!matched && ((target  & SessionSearchOptions.Server) == SessionSearchOptions.Server))
            {
                string serverName = this.ServerName;
                if (null == serverName)
                {
                    serverName = string.Empty;
                }
                matched = serverName.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0;
            }

            return matched;
        }

        /// <summary>
        /// Returns the model object associated with this v-m.
        /// </summary>
        public ISessionModel Model
        {
            get { return this.sessionModel; }
        }

        /// <summary>
        /// Used to notify us that the session selection mode has been changed. We use this to
        /// appearance and behaviour of sessions in the session list by swapping between using a
        /// button or checkbox as the session container. These two controls give us the two
        /// different selection modes we use.
        /// </summary>
        public void NotifySelectionModeChanged()
        {
            // When the toggle select mode is deactivated, clear the "is selected" state.
            if (!this.vmRoot.SessionsViewModel.IsToggleSelectActive)
            {
                this.IsSelectedForEdit = false;
            }
            OnPropertyChanged("EnableToggleSelect");
        }

        /// <summary>
        /// Command to refresh the state of this session.
        /// </summary>
        public DelegateCommand RefreshCommand
        {
            get;
            private set;
        }

        /// <summary>
        /// Launches the Microsoft Remote Assistance tool and pre-populates it with the details
        /// of this session, to the user can easily start a remote assistance session.
        /// </summary>
        public DelegateCommand RemoteAssistCommand
        {
            get;
            private set;
        }

        /// <summary>
        /// Command to reset this session.
        /// </summary>
        public DelegateCommand ResetCommand
        {
            get;
            private set;
        }

        /// <summary>
        /// Returns the name of the XenApp server the session is running on.
        /// </summary>
        public string ServerName
        {
            get { return this.sessionModel.ServerName; }
        }

        /// <summary>
        /// The Windows session ID.
        /// </summary>
        public int? SessionId
        {
            get { return this.sessionModel.SessionId; }
        }

        /// <summary>
        /// Returns the detailed set of session properties. These will only be fully populated
        /// once the full session state has been requested from the server.
        /// </summary>
        public IEnumerable<Tuple<string, string>> SessionDetails
        {
            get
            {
                lock (this.stateLock)
                {
                    return new List<Tuple<string, string>>(this.sessionDetails);
                }
            }
        }

        /// <summary>
        /// The current session state.
        /// </summary>
        public SessionConnectionState State
        {
            get
            {
                return this.sessionModel.State;
            }
        }

        /// <summary>
        /// Handler for property changes on the associated model object. Note: maybe called on
        /// any thread.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SessionModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "SessionActionStatus")
            {
                this.vmRoot.UiDispatch(() => OnPropertyChanged("IsEnabled"));
            }
            else if (e.PropertyName == "AccountName")
            {
                this.vmRoot.UiDispatch(() => OnPropertyChanged("AccountName"));
            }
            else if (e.PropertyName == "BrowserName")
            {
                this.vmRoot.UiDispatch(() => OnPropertyChanged("BrowserName"));
            }
            else if (e.PropertyName == "ServerName")
            {
                this.vmRoot.UiDispatch(() => OnPropertyChanged("ServerName"));
            }
            else if (e.PropertyName == "SessionId")
            {
                this.vmRoot.UiDispatch(() => OnPropertyChanged("SessionId"));
            }
            else if (e.PropertyName == "State")
            {
                this.vmRoot.UiDispatch(() => OnPropertyChanged("State"));
            }
            else if (e.PropertyName == "IsLoading")
            {
                this.loadingStatus.NotifyChanged();
            }

            CreateSessionDetails();
        }

        /// <summary>
        /// Builds the set of session details.
        /// </summary>
        /// <remarks>
        /// The details are roughly grouped according to how useful they are.
        /// </remarks>
        private void CreateSessionDetails()
        {
            lock (this.stateLock)
            {
                this.sessionDetails = new List<Tuple<string, string>>();
                Push(this.sessionDetails, Properties.Resources.SessionAccountName, this.Model.AccountName);
                Push(this.sessionDetails, Properties.Resources.SessionAppName, this.Model.BrowserName);
                Push(this.sessionDetails, Properties.Resources.SessionId, this.Model.SessionId);
                Push(this.sessionDetails, Properties.Resources.SessionState, this.Model.State);
                Push(this.sessionDetails, Properties.Resources.SessionAppState, this.Model.ApplicationState);
                Push(this.sessionDetails, Properties.Resources.SessionServerName, this.Model.ServerName);

                Push(this.sessionDetails, Properties.Resources.SessionConnectTime, this.Model.ConnectTime);
                Push(this.sessionDetails, Properties.Resources.SessionDisconnectTime, this.Model.DisconnectTime);
                Push(this.sessionDetails, Properties.Resources.SessionLogonTime, this.Model.LogOnTime);
                Push(this.sessionDetails, Properties.Resources.SessionLastInputTime, this.Model.LastInputTime);

                Push(this.sessionDetails, Properties.Resources.SessionClientAddress, this.Model.ClientAddress);
                Push(this.sessionDetails, Properties.Resources.SessionClientName, this.Model.ClientName);
                Push(this.sessionDetails, Properties.Resources.SessionClientProductId, this.Model.ClientProductId);
                Push(this.sessionDetails, Properties.Resources.SessionColorDepth, this.Model.ColorDepth);
                Push(this.sessionDetails, Properties.Resources.SessionResolution, string.Format("{0}x{1}", this.Model.HorizontalResolution, this.Model.VerticalResolution));
                Push(this.sessionDetails, Properties.Resources.SessionName, this.Model.SessionName);

                Push(this.sessionDetails, Properties.Resources.SessionAccessGuid, this.Model.AccessSessionGuid);
                Push(this.sessionDetails, Properties.Resources.SessionClientBuffers, this.Model.ClientBuffers);
                Push(this.sessionDetails, Properties.Resources.SessionClientBuildNum, this.Model.ClientBuildNumber);
                Push(this.sessionDetails, Properties.Resources.SessionClientCacheDisk, this.Model.ClientCacheDisk);
                Push(this.sessionDetails, Properties.Resources.SessionClientCacheLow, this.Model.ClientCacheLow);
                Push(this.sessionDetails, Properties.Resources.SessionClientMinBitmap, this.Model.ClientCacheMinBitmapSize);
                Push(this.sessionDetails, Properties.Resources.SessionClientCacheSize, this.Model.ClientCacheSize);
                Push(this.sessionDetails, Properties.Resources.SessionClientCacheTiny, this.Model.ClientCacheTiny);
                Push(this.sessionDetails, Properties.Resources.SessionClientCacheXms, this.Model.ClientCacheXms);
                Push(this.sessionDetails, Properties.Resources.SessionClientDirectory, this.Model.ClientDirectory);
                Push(this.sessionDetails, Properties.Resources.SessionClientId, this.Model.ClientId);
                Push(this.sessionDetails, Properties.Resources.SessionClientIPV4, this.Model.ClientIPV4);
                Push(this.sessionDetails, Properties.Resources.SessionClientType, this.Model.ClientType);

                Push(this.sessionDetails, Properties.Resources.SessionDirectXEnabled, this.Model.DirectXEnabled);
                Push(this.sessionDetails, Properties.Resources.SessionEncryptionLevel, this.Model.EncryptionLevel);
                Push(this.sessionDetails, Properties.Resources.SessionFlashEnabled, this.Model.FlashEnabled);
                Push(this.sessionDetails, Properties.Resources.SessionProtocol, this.Model.Protocol);
                Push(this.sessionDetails, Properties.Resources.SessionServerBuffers, this.Model.ServerBuffers);
                Push(this.sessionDetails, Properties.Resources.SessionUsbEnabled, this.Model.UsbEnabled);
                Push(this.sessionDetails, Properties.Resources.SessionVirtualIp, this.Model.VirtualIP);
                Push(this.sessionDetails, Properties.Resources.SessionWmpEnabled, this.Model.WmpEnabled);
            }

            this.vmRoot.UiDispatch(() => OnPropertyChanged("SessionDetails"));
        }

        /// <summary>
        /// Adds a string type property to the set of detailed properties.
        /// </summary>
        /// <param name="details">The set to store in.</param>
        /// <param name="key">The descriptive key.</param>
        /// <param name="value">The value.</param>
        private static void Push(List<Tuple<string, string>> details, string key, string value)
        {
            details.Add(new Tuple<string,string>(key, value));
        }

        /// <summary>
        /// Adds a DateTime type property to the set of detailed properties.
        /// </summary>
        /// <param name="details">The set to store in.</param>
        /// <param name="key">The descriptive key.</param>
        /// <param name="value">The value.</param>
        private static void Push(List<Tuple<string, string>> details, string key, DateTime? value)
        {
            string formatted = null;
            if (value.HasValue)
            {
                DateTime time = value.Value;
                if (time.Kind == DateTimeKind.Utc)
                {
                    time = time.ToLocalTime();
                }
                formatted = time.ToString();
            }

            details.Add(new Tuple<string, string>(key, value.ToString()));
        }

        /// <summary>
        /// Adds a struct type property (e.g. int, enum) to the set of detailed properties.
        /// </summary>
        /// <param name="details">The set to store in.</param>
        /// <param name="key">The descriptive key.</param>
        /// <param name="value">The value.</param>
        /// <typeparam name="T">The type of the property, must be based on a struct. The value
        /// to display is retrieved using the ToString override.</typeparam>
        private static void Push<T>(List<Tuple<string, string>> details, string key, T value) where T : struct
        {
            details.Add(new Tuple<string, string>(key, value.ToString()));
        }

        /// <summary>
        /// Adds a nullable struct type property to the set of detailed properties.
        /// </summary>
        /// <param name="details">The set to store in.</param>
        /// <param name="key">The descriptive key.</param>
        /// <param name="value">The value.</param>
        /// <typeparam name="T">The type of the property, must be based on a struct. The value
        /// to display is retrieved using the ToString override.</typeparam>
        private static void Push<T>(List<Tuple<string, string>> details, string key, Nullable<T> value) where T : struct
        {
            details.Add(new Tuple<string, string>(key, value.ToString()));
        }

        /// <summary>
        /// Initiates remote assistance of this session.
        /// </summary>
        private void RemoteAssistSession()
        {
            string fullAccount = this.sessionModel.AccountName;

            if (!string.IsNullOrEmpty(fullAccount))
            {
                string domain = null;
                string user = fullAccount;

                if (fullAccount.Contains('\\'))
                {
                    var result = fullAccount.Split('\\');
                    if (result != null && result.Length == 2)
                    {
                        domain = result[0];
                        user = result[1];
                    }
                    else
                    {
                        domain = null;
                        user = fullAccount;
                    }
                }

                MSRA.RemoteAssist.LaunchMsra(new MSRA.SessionDetails
                    {
                        DomainName = domain,
                        ServerName = this.sessionModel.ServerName,
                        SessionID = this.sessionModel.SessionId.ToString(),
                        UserName = user
                    });
            }
        }

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Fired whenever a public property changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Fires the property change event. Maybe called from any thread - we always fire the
        /// event on the main UI thread.
        /// </summary>
        /// <param name="propertyName">The name of the property that changed.</param>
        private void OnPropertyChanged(string propertyName)
        {
            NotifyHelper.VerifyProperty(propertyName, this);
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion // INotifyPropertyChanged Members

        #region IPage members

        /// <summary>
        /// Called when this v-m is activated/deactivated as the current page (when displayed
        /// in SessionDetailsView).
        /// </summary>
        /// <param name="isActive"></param>
        public void NotifyActiveChanged(bool isActive)
        {
            if (isActive)
            {
                this.vmRoot.SessionsViewModel.NotifySessionSelected(this);

                // When showing a session, do a refresh of the state.
                this.sessionModel.Refresh(true);
            }
        }

        /// <summary>
        /// Called when the user presses the back button when this page is active.
        /// </summary>
        public void NotifyBackButtonPressed()
        {
            this.vmRoot.DefaultBackCommand.Execute();
        }

        /// <summary>
        /// The v-m object.
        /// </summary>
        public ViewModelManager VmManager
        {
            get { return this.vmRoot; }
        }

        #endregion
    }
}
